Skip to content

feat: compile per-series vector arithmetic and match Prometheus summation - #489

Closed
zzylol wants to merge 59 commits into
feat/kernel-codec-accessorsfrom
feat/per-series-binary
Closed

zzylol wants to merge 59 commits into
feat/kernel-codec-accessorsfrom
feat/per-series-binary

Conversation

@zzylol

@zzylol zzylol commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #495.

Why

ASAPQuery-backend #798/#799 run all query-side computation as Planner physical DAGs and forward shapes Planner cannot compile to the exact engine. Per-series arithmetic over stored readouts, such as avg_over_time(x[5m]) (sum/count), rate(a)/rate(b) and rate(x)*2, was rejected, so these queries are no longer served from stored state. Separately, logical selection panicked on sum without (pod) (m) and quantile without (pod) (0.5, m). Planner's sums and averages also differed from Prometheus in the last bits and on overflow.

What / How

  • Per-series Binary: when either input carries $promql_series_identity, a query-time Binary compiles to feat: compile multi-selector PromQL subtrees and more range functions #487's series_labels for each side plus series_binary. Matching is one-to-one on labels without __name__, and honors on/ignoring when the IR carries them. A duplicate match group is an error, an empty side gives an empty result, and division is IEEE (±Inf/NaN); the checked-division flags are kept. A literal operand relabels (dropping __name__) and then projects.
  • without state column: summary_col_index used the excluded-key count as the state index, so the family was never written into the schema and export panicked with SummaryFamilySchemaMismatch. The state column now comes after the kept labels.
  • Summation: grouped sum/avg (which includes current-series readouts and SQL Float64 SUM/AVG) and sum_over_time/avg_over_time now use Prometheus' Kahan–Neumaier kahansum.Inc. Averages switch to Prometheus' incremental mean once the running sum would overflow. Stored exact Sum state is unchanged because compensating it would change the state layout; this is listed under Remaining in the doc.
  • Updated docs/develop_docs/physical-compile-coverage.md: row 5 is now Supported (20/4/5/2).

Before this PR

  • compile on the selected DAG for rate(a[5m]) / rate(b[5m]) failed with row binary requires grouped rows; per-series matching needs a name-free identity.
  • Candidate enumeration for sum without (pod) (m) panicked with compiler emits a valid post-ASAP DAG: SummaryFamilySchemaMismatch.
  • sum_over_time over [1e100, 1, -1e100] returned 0, and avg(m) over two series of 1.7e308 returned +Inf.

After this PR

  • rate(a)/rate(b) returns {instance="x",job="api"} 2, web gives +Inf (x/0) and cache gives NaN (0/0); unmatched series are dropped. avg_over_time(m[5m]) returns the per-series means with no __name__.
  • Every without candidate exports a valid DAG.
  • The two cases above return 1 and 1.7e308, matching Prometheus.

Validation

  • Tests were written first and failed before each fix: deployment_computation (per-series average, rate ratio, scalar, on/ignoring with duplicates, population sums), promql_fallback (compensated sums and averages), and promql_to_post_asap (without candidates).
  • cargo fmt --check, cargo clippy --workspace --all-targets -- -D warnings, cargo test --workspace --no-fail-fast: 1386 passed, 0 failed.
  • An independent reviewer agent found no correctness bugs. Its low-severity findings (a comment, docs, test gaps) are addressed in the last commit.

🤖 Generated with Claude Code

zzylol and others added 29 commits September 30, 2026 18:02
Start `asap-physical-operators` with thin summary kernels over
`asap_sketchlib`, the kernel capability checks and the typed value model.

The boundary is:
- `asap_sketchlib` owns sketch algorithms and their state encodings.
- Kernels hold one population's in-memory state. They expose `merge`, a
  typed sketch readout (`estimate(&SketchQuery)`) and memory accounting.
  Exact states answer a typed `ExactReadout`; empty MIN/MAX read as `None`.
- Group-by belongs to physical operators.
- Deployments own wire decoding, delta frames, edge sampling and storage
  statistics.

So wire decoding, `SerializableToSink`, `AggregationType`, `aux_stats`,
`reset_to_empty` and the keyed/sum/min/max kernels are not carried over from
ASAPQuery-backend. The `asap_sketch_codec` crate is not carried over either;
it moves to `asap_sketchlib`. Hydra KLL remains as the Hydra shared-grouping
kernel. HLL uses sketchlib's classic estimator.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Rebased onto main: the Cargo.lock conflict with #478 (new asap-planner
crate) was resolved by regenerating workspace entries with
`cargo update -w --offline`.
Add the execution layer of `asap-physical-operators`:

- `plan`, `runtime` and `sources`: physical DAGs, run context, bounded
  backpressure, cancellation, memory reservations and raw-source scans.
- `expressions` and `operators`: relational/scalar operators, windows,
  temporal panes, current-series snapshots, and summary build, merge and
  readout. A readout is typed: a `SketchQuery`, or an `ExactReadout` whose
  counter lookback resolves to the run's evaluation range. Deserialized
  operators are validated before use.
- `readout`: readouts over merged exact summary states.

There is no physical planner yet; operators are built directly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add `physical_planner`: reader-independent compilation of selected logical
candidates into precompute/query physical DAGs with typed materialization
frontiers, bounded frontier enumeration, workload cost selection, temporal
KLL pane compilation and PromQL row/value lowering. `dag` remains a
compatibility re-export.

Tests cover physical DAGs, plan recovery, precompute candidates and
populations, PromQL values and binaries, weighted TopK binding and
current-series heaps, plus Planner-to-execution integration tests. The design
doc describes the ownership boundary with deployments.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pane geometry, pane population checks and per-pane scheduling are deployment
concerns. The physical layer keeps the computation the deployment binds:
per-input summary build, union, shared merge and readout.

- Remove `physical_planner::compile_temporal_pane_candidate` and its
  `TemporalPaneMaintenance`/`TemporalPaneCandidate`/`TemporalEntityIdentity`
  contract.
- Remove the `PaneInput` operator; `ScopeTimestamp` remains as a general
  operator in `operators/scope_timestamp`.
- Drop the `selected_temporal_lifecycle_compiles_panes_and_executes` E2E test
  and update the crate README and design doc.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Planner search needs the physical row representation to propose whole-root
physical alternatives, and asap-aware-mapping cannot depend on the physical
runtime crate. promql_rows::with_series_identity keeps its signature and
delegates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The backend built one workload forest per per-query physical alternative by
calling SketchAlgorithmStrategy's proposal methods outside PlanSpace. Planner
now owns them: search_workload_with_targets asks each strategy's new
ReplacementStrategy::propose_for_root once per targeted root.
SketchAlgorithmStrategy resolves the series identity and proposes
current-series TopK, fixed-window and query-time Rate aggregation, and
realizations over per-series Rate state, finalized and deduplicated.

The candidates carry ReplacementProvenance::RootPhysicalRealization: DAG
assembly uses them verbatim, and global selection never commits them.
Existing candidates and their order are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PlanSpace decides what to compute; the summary maintenance lifecycle decides
node timing and the physical compiler reads it. The fixed-window and
query-time Rate aggregation candidates only rewrote a finalization node's
timing, so they are placement variants and no longer go through
propose_for_root. The backend still calls those methods directly.

SketchAlgorithmStrategy::propose_for_root now proposes only current-series
TopK heaps, which rank identity-carrying rows the logical root lacks. The
per-series Rate filter and the verbatim-assembly branch are removed: the
latter is unnecessary because non-Aggregate roots are already assembled
verbatim. RootPhysicalRealization stays so global selection never commits
an unvalidated identity-carrying readout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nding

Split plan_summary_maintenance_lifecycles into enumeration and selection so
a deployment can price every lifecycle alternative per unique summary state
and bind its own choice. Planner selection is unchanged: it now enumerates
and then selects the cheapest complete combination through the same path.

SummaryMaintenanceLifecycleCandidates::select validates that each choice is
an alternative Planner could select, enforces schedule compatibility, and
obtains window frameworks and totals from the same complete-candidate hook.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A chosen summary-maintenance lifecycle did not reach the DAG: timing was
fixed by realization strategies. SummaryMaintenanceLifecyclePlan::
execution_timed_dag assigns every node's timing from the selection: retained
(non-Ephemeral) states and their inputs at ingestion time, everything else at
query time. States without a selected lifecycle, and maintained populations
that lifecycle enumeration does not cover, are refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rebase note: #483 removed the pane test this test was appended after; the
new test is appended to the #483 version of the file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replace the open question about placement ownership with the agreed four-layer
contract: logical Post-ASAP decides what to compute, the chosen lifecycle
decides timing, physical compilation partitions by timing, and the backend
prices lifecycle choices including store cost.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`compile_candidate` re-lowered the whole Post-ASAP DAG for every
materialization frontier, and `enumerate_frontiers` compiled it once more.
Number helper operators from their Planner node (`u64::MAX - node_id`,
at most one helper per node) so every boundary choice is a subgraph of
one lowering. `cut_candidate` partitions a `compile` result for one
frontier and `enumerate_compiled_frontiers` enumerates over it; both
produce candidates byte-identical to per-frontier recompilation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The #462 split no longer exposes PhysicalCandidate::encode; its serde form
gives the same byte-for-byte comparison.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The lifecycle layer assigns each node's timing; physical compilation now
reads it. `frontier_from_timing` returns the ingestion-time nodes read by
query-time nodes (or an ingestion-time root) and rejects a query-time node
feeding an ingestion-time one, so each lifecycle assignment is a
`cut_candidate` of one `compile` result. `enumerate_compiled_frontiers` is
private: placement comes from timing, and its only caller is
`enumerate_frontiers`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
For the KLL quantile and grouped Rate->Sum fixtures, ContinuouslyMaintained
and Ephemeral timed DAGs cut one compilation into exactly the candidates
`compile_candidate` builds. The hand-written timing frontier in the chosen
lifecycle test now uses `frontier_from_timing`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Number helpers as u64::MAX - (node << 16) - index so a node that lowers to
an operator chain keeps deterministic, traversal-independent helper IDs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With several retained states, only those read by a query-time node or
forming the root are cut points.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DAG assembly replaced any selected outer Sum over an inner aggregate with a
query-time exact Sum, even when the selected summary realizes the inner Rate
itself. The grouped Sum state therefore never reached the inventory, and no
lifecycle choice could move grouped Sum into precompute.

Assembly now keeps such a selected summary; the query-time residual still
applies when the outer summary would hide its inner aggregate in KeepPreAsap.
Default selection for sum by(job)(rate(...)) now yields Rate -> grouped Sum
state; the physical frontier test reads its query root accordingly.

Conflicts with earlier stack changes resolved to the integration tree:
- crates/integration-tests/tests/summary_maintenance_lifecycle_e2e.rs: c98281a Merge remote-tracking branch 'origin/feat/compile-once-cuts' into integration/planner-for-backend

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SketchAlgorithmStrategy::fixed_window_rate_candidates and
query_time_rate_aggregation_candidates returned the same logical DAG as the
ordinary heap or grouped Sum candidate with Rate finalization flipped between
ingestion and query time. Placement now comes only from a chosen lifecycle via
SummaryMaintenanceLifecyclePlan::execution_timed_dag.

compile_fixed_window_rate_aggregation takes that lifecycle-timed PostAsapDag
instead of a SummaryNode with baked-in timing. The fixed-window heap test binds
continuously maintained lifecycles; the grouped Sum placement pair is covered
by the lifecycle end-to-end test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
execution_timed_dag refused any plan with a MaintainPopulation node because
lifecycle enumeration covered SummaryAgg states only, so population timing
stayed fixed by the realization strategy.

Enumeration now also emits one deployment per unique maintained population
that does not feed a SummaryAgg, with the usual alternatives costed through
the caller's lifecycle hooks (unknown stays unknown). select and
execution_timed_dag treat it like summary state: retained at ingestion with
its raw input, Ephemeral rebuilt from raw input at query time. A population
feeding a SummaryAgg is that state's input and follows its timing. A plan
whose population deployment was removed is still refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The execution-data-state validator required MaintainPopulation at ingestion
time, and MaintainedPopulationStrategy hard-coded it there. Timing is now the
lifecycle's choice: the validator accepts either timing and keeps the
structural contracts (the population reads its matching raw input; its
readout is query-time over a population that supports it). The strategy
writes query time as the initial layout, as other realization strategies do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts with earlier stack changes resolved to the integration tree:
- crates/integration-tests/tests/summary_maintenance_lifecycle_e2e.rs: 51fd198 Merge #491 population lifecycle into Planner integration

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zzylol and others added 15 commits September 30, 2026 18:09
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Evaluates instant selection and range functions over left-open windows
at the query time or on a subquery step grid.

Conflicts with earlier stack changes resolved to the integration tree:
- crates/asap-physical-operators/src/operators/mod.rs: a7ff3ae Merge remote-tracking branch 'origin/feat/physical-compile-promql-fallback' into integration/planner-for-backend

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts with earlier stack changes resolved to the integration tree:
- crates/asap-physical-operators/src/physical_planner/promql_rows.rs: a7ff3ae Merge remote-tracking branch 'origin/feat/physical-compile-promql-fallback' into integration/planner-for-backend

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tile over time

Per-series windows evaluate these PromQL range functions with Prometheus
semantics over fresh (non-stale) samples.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, without and @

Each Fallback selector reads its own raw-series slot. Vector-vector
arithmetic uses PromQL one-to-one matching with on/ignoring via new
series_labels and series_binary operators; without grouping rewrites the
series identity; @ <timestamp> fixes selector and subquery evaluation.

Conflicts with earlier stack changes resolved to the integration tree:
- crates/asap-physical-operators/src/operators/mod.rs: 9a13ae4 integrate: extend asap-types series identity with #486/#487 shapes
- crates/asap-physical-operators/src/physical_planner/promql_rows.rs: 9a13ae4 integrate: extend asap-types series identity with #486/#487 shapes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…antile IR needs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#477 moved PromQL series-identity resolution into asap-types. The
fallback shapes compiled by #486 and #487 (time shifts, subqueries,
scalar bridges, and arithmetic between series) also need identity
realization there. Taken from integration commit 9a13ae4.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A precompute boundary at a raw time-series scan binds as raw sample rows
($population label map, $timestamp, value). The label map is the complete
source identity, so per-series and grouped SummaryAgg lower over it with
constant, column or unit-frequency (HLL) updates, and keyed heaps resolve
their items from labels, the sample value or the canonical label identity.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Raw sample rows drop empty label values so one series has one population.
Heap items resolve names against the scan (the value column, the series
identity, labels; other scan columns are rejected), and identity items may
exclude labels, as `topk by` emits. Unit-frequency HLL updates are raw-only
and never apply to keyed families. Tests cover Planner-generated heaps,
missing and empty labels, and `without` grouping.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
State the canonical label-set obligation and assert per-query coverage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Deployments persisting ExactAccumulator had to mirror its serde shape to
reject payloads whose population states differ from the declared family.
Deserialization now performs that check itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…outs

The backend kept a UnivMon shim because Planner's accumulator hid its
sketch and answered no readouts. from_sketch/sketch let a deployment
encode and restore it with sketchlib's codec; estimate answers count,
distinct, L2 and entropy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The backend reached the sketchlib kernel through a msgpack round trip to
persist and validate it. Public algorithm/shape and to_bytes/from_bytes
(delegating to sketchlib's versioned format) remove that.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Backend ingest deltas and read-path merges cloned each cached sketch to
update it. A mutable downcast lets them update it in place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@zzylol
zzylol force-pushed the feat/physical-compile-coverage-3 branch from eb34e4f to d4375b3 Compare September 30, 2026 18:27
@zzylol
zzylol force-pushed the feat/per-series-binary branch from ab6c642 to 3f0d2e2 Compare September 30, 2026 18:27
@zzylol
zzylol changed the base branch from feat/physical-compile-coverage-3 to feat/kernel-codec-accessors September 30, 2026 18:28
zzylol and others added 5 commits September 30, 2026 18:37
A without grouping's keys are the excluded labels, so the state column
index was the excluded-key count, which could fall past the kept labels.
The family was then never written into the schema and exporting the
candidate panicked with SummaryFamilySchemaMismatch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…outs

Query-time Binary nodes over rows with a series identity, such as
avg_over_time as stored sum/count or rate(a)/rate(b), now compile to
series_labels and series_binary: one-to-one matching on labels without
the metric name, honoring on/ignoring when the IR carries them. A literal
operand drops the metric name and applies to each value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Grouped sum/avg (including current-series readouts) and sum/avg_over_time
now use Kahan-Neumaier summation. An average switches to Prometheus'
incremental mean once the running sum would overflow, so it no longer
becomes +Inf where Prometheus returns a finite mean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y docs

Addresses review: pin Inf/-Inf sum and average cases, show that literal
arithmetic drops __name__ from a stored readout, correct the zero-start
comment, and note one-to-one-only coverage and the SQL SUM/AVG change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@zzylol
zzylol force-pushed the feat/kernel-codec-accessors branch from b945081 to aebe2ea Compare September 30, 2026 20:24
@zzylol
zzylol force-pushed the feat/per-series-binary branch from 3f0d2e2 to 000c1ef Compare September 30, 2026 20:24
@zzylol
zzylol force-pushed the feat/kernel-codec-accessors branch 2 times, most recently from b8440f7 to a9db8d7 Compare October 1, 2026 23:06
zzylol added a commit that referenced this pull request Oct 1, 2026
…togram_quantile (#489, #490, #492, #494, #493)

Per-series vector arithmetic with Prometheus summation, duplicate label-set rejection, comparisons and set operators, rejection of fill modifiers, and histogram_quantile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@zzylol

zzylol commented Oct 1, 2026

Copy link
Copy Markdown
Contributor Author

Included in the squash merge of #493 into main (deb3904).

@zzylol zzylol closed this Oct 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant